﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics.Contracts;

namespace Common
{
    /// <summary>
    /// A dictionary which forgets unused values over time
    /// </summary>
    /// <remarks>
    /// The name "half life" comes from radioactive decay where it is not possible to predict the exact time 
    /// a given particle will decay, only that statisically speaking we know they decay at a certain rate.  
    /// Values in this collection are guarenteed to live at least as long as the half life and at most 
    /// twice as long as the half life (unless they are manually removed early or refreshed of course)
    /// </remarks>
    /// <typeparam name="TKey">The key type</typeparam>
    /// <typeparam name="TValue">The value type</typeparam>
    public class HalfLifeDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IDisposable
    {
        private IDictionary<TKey, TValue> _current = new Dictionary<TKey, TValue>();
        private IDictionary<TKey, TValue> _old = new Dictionary<TKey, TValue>();

        private object synclock = new object();

        private Timer _halfLifeTimer;

        private static readonly TimeSpan DEFAULT_HALFLIFE = TimeSpan.FromMinutes(30);

        /// <summary>
        /// Default constructor uses 30 minute half life
        /// </summary>
        public HalfLifeDictionary()
            : this(DEFAULT_HALFLIFE)
        { }
        /// <summary>
        /// Constructs a half life cache using the specified half life
        /// </summary>
        /// <param name="halfLife">The half life to use</param>
        public HalfLifeDictionary(TimeSpan halfLife)
        {
            _halfLifeTimer = new Timer(this.HalfLifeCycle);
            HalfLife = halfLife;
        }

        private void HalfLifeCycle(Object ignored)
        {
            lock (synclock)
            {
                _old.Clear();
                IDictionary<TKey, TValue> temp = _old;
                _old = _current;
                _current = temp;
            }
        }

        private TimeSpan _halfLife;
        /// <summary>
        /// The half life of this collection.
        /// </summary>
        /// <remarks>WARNING: changing the halflife of the collection resets the timer.  You should only call this property once when you are creating it</remarks>
        public TimeSpan HalfLife
        {
            get { return _halfLife; }
            set
            {
                _halfLifeTimer.Change(TimeSpan.Zero, value);
                _halfLife = value;
            }
        }

        /// <summary>
        /// Adds a key/value to the collection
        /// </summary>
        /// <param name="key">The key to store the value under</param>
        /// <param name="value">The value to be stored</param>
        public void Add(TKey key, TValue value)
        {
            lock (synclock)
            {
                _current.Add(key, value);
                _old.Remove(key);
                Contract.Assume(Count >= Contract.OldValue(Count));
            }
        }

        /// <summary>
        /// Returns true if the collection contains the specified key
        /// </summary>
        /// <param name="key">The key to check for</param>
        /// <returns>True if the collection contains the specified key</returns>
        public bool ContainsKey(TKey key)
        {
            lock (synclock)
            { 
                bool result = _current.ContainsKey(key) || _old.ContainsKey(key);
                Contract.Assume(!result || Count > 0);
                return result;
            }
        }

        /// <summary>
        /// Returns a snapshot of the keys in this collection
        /// </summary>
        public ICollection<TKey> Keys
        {
            get
            {
                lock (synclock)
                { return _current.Keys.Concat(_old.Keys).ToList(); }
            }
        }

        /// <summary>
        /// removes a key from the collection
        /// </summary>
        /// <param name="key">The key to be removed</param>
        /// <returns>True if the key had actually been present</returns>
        public bool Remove(TKey key)
        {
            lock (synclock)
            {
                bool success;
                success = _current.Remove(key);
                success |= _old.Remove(key);
                return success;
            }
        }

        /// <summary>
        /// Tries to fetch a value from the collection
        /// </summary>
        /// <param name="key">The key to search under</param>
        /// <param name="value">An output variable to hold the result if found (if not found value is undefined)</param>
        /// <returns>True if the value was found</returns>
        public bool TryGetValue(TKey key, out TValue value)
        {
            lock (synclock)
            {
                bool result;
                if (_current.TryGetValue(key, out value))
                { result = true; }
                else
                { result = _old.TryGetValue(key, out value); }
                Contract.Assume(result == ContainsKey(key));
                return result;
            }
        }

        /// <summary>
        /// Returns a snapshot of the values in the collection
        /// </summary>
        public ICollection<TValue> Values
        {
            get
            {
                lock (synclock)
                { return _current.Values.Concat(_old.Values).ToList(); }
            }
        }

        /// <summary>
        /// Gets/Set a value in the collection
        /// </summary>
        /// <remarks>
        /// WARNING: Due to the way this class operates, fetching values using this method could easily fail even if you just checked for the value.  Use TryGetValue to fetch values.
        /// </remarks>
        /// <param name="key">The key to look under</param>
        /// <returns>The value if found</returns>
        public TValue this[TKey key]
        {
            get
            {
                lock (synclock)
                {
                    if (_current.ContainsKey(key))
                    { return _current[key]; }
                    else
                    { return _old[key]; }
                }
            }
            set
            {
                lock (synclock)
                {
                    _current[key] = value;
                    _old.Remove(key);
                }
            }
        }

        /// <summary>
        /// Adds a key value pair to the collection
        /// </summary>
        /// <param name="item">The key value pair to add</param>
        public void Add(KeyValuePair<TKey, TValue> item)
        {
            lock (synclock)
            {
                _current.Add(item);
                _old.Remove(item.Key);
                Contract.Assume(this.Count >= Contract.OldValue(this.Count));
            }
        }

        /// <summary>
        /// remove all keys and values from this collection
        /// </summary>
        public void Clear()
        {
            lock (synclock)
            {
                _current.Clear();
                _old.Clear();
                Contract.Assume(Count == 0);
            }
        }

        /// <summary>
        /// Returns true if the collection contains the specified key value pair
        /// </summary>
        /// <param name="item">The key value pair to check for</param>
        /// <returns>True if the collection contains the specified key value pair</returns>
        public bool Contains(KeyValuePair<TKey, TValue> item)
        {
            lock (synclock)
            { 
                bool result = _current.Contains(item) || _old.Contains(item);
                Contract.Assume(!result || Count > 0);
                return result;
            }
        }

        /// <summary>
        /// Copies the key value pairs in this collection to the specified array
        /// </summary>
        /// <param name="array">The array to copy to</param>
        /// <param name="arrayIndex">The offset to start copying from</param>
        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            lock (synclock)
            {
                List<KeyValuePair<TKey, TValue>> temp = _current.ToList().Concat(_old.ToList()).ToList();
                Contract.Assume(arrayIndex < array.Length);
                Contract.Assume(arrayIndex + temp.Count <= array.Length);
                temp.CopyTo(array, arrayIndex);
            }
        }

        /// <summary>
        /// Returns the number of items in the collection
        /// </summary>
        public int Count
        {
            get
            {
                lock (synclock)
                { return _current.Count + _old.Count; }
            }
        }

        /// <summary>
        /// Returns false
        /// </summary>
        public bool IsReadOnly
        {
            get { return false; }
        }

        /// <summary>
        /// Removes a key value pair from the collection
        /// </summary>
        /// <param name="item">The key value pair to remove</param>
        /// <returns>True if the key value pair had actually been present</returns>
        public bool Remove(KeyValuePair<TKey, TValue> item)
        {
            lock (synclock)
            { return _current.Remove(item) || _old.Remove(item); }
        }

        /// <summary>
        /// Returns a snapshot enumerator for this collection
        /// </summary>
        /// <returns>A snapshot enumerator for this collection</returns>
        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            return this.ToList().GetEnumerator();
        }

        /// <summary>
        /// Returns a snapshot enumerator for this collection
        /// </summary>
        /// <returns>A snapshot enumerator for this collection</returns>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.ToList().GetEnumerator();
        }

        /// <summary>
        /// Cleans up the timer used for removing keys
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Cleans up the timer used for removing keys
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "all")]
        protected virtual void Dispose(bool all)
        {
            if (_halfLifeTimer != null)
            {
                _halfLifeTimer.Dispose();
                _halfLifeTimer = null;
            }
        }

        [ContractInvariantMethod]
        private void InvariantMethod()
        {
            Contract.Invariant(_current != null);
            Contract.Invariant(_old != null);
            Contract.Invariant(synclock != null);
            Contract.Invariant(_halfLifeTimer != null);
        }
    }
}
